#pragma onece
#pragma warning(disable:4251)

#include <algorithm>
#include <string>
#include <vector>
#include <functional>
#include <map>
#include <memory>
#include <mutex>

template <typename CallbackT>
class NotificationHelper
{
public:
    using Callback = CallbackT;
    using CallbackWeekPtr = std::weak_ptr<CallbackT>;
    using CallbackPtr = std::shared_ptr<CallbackT>;
    using CallbackList = std::vector<CallbackWeekPtr>;

    virtual ~NotificationHelper() = default;

    void registerCallback(CallbackPtr callback)
    {
        if (callback == nullptr)    
        {
            return;
        }

        std::lock_guard<std::mutex> guard(mDataLock);
        auto iter = std::find_if(mCallbacks.begin(), mCallbacks.end(), [callback](CallbackWeekPtr mWeakcallback) {
            if(auto mCallback = mWeakcallback.lock())
            {
                return mCallback == callback;
            }
            return false; 
        });
            
        if (iter != mCallbacks.end())
        {
            mCallbacks.erase(iter);   
        }
    }

    size_t getRegisteredCallbackCount()
    {
        std::lock_guard<std::mutex> guard(mDataLock);
        return mCallbacks.size();
    }    

protected:
    CallbackList mCallbacks;
    std::mutex mDataLock;

    using Closure = std::function<void(std::shared_ptr<Callback>)>;
    void potentiallyFireNotification(Closure closure, std::function<bool(CallbackWeekPtr)> shouldFire = [](CallbackWeekPtr) -> bool{return true;})
    {
        CallbackList liveCallbacks = removeExpiredCallbacks();
        for (auto callback : liveCallbacks)
        {
            if(auto lockedCallback = callback.lock())
            {
                if (shouldFire(callback))
                {
                    closure(lockedCallback);
                }
            }   
        }
    }

    template <typename Func, typename... Args>
    void fireNotification(Func func, Args&&... args)
    {
        CallbackList liveCallbacks = removeExpiredCallbacks();
        for (auto callback : liveCallbacks)
        {
            if(auto lockedCallback = callback.lock())
            {
                std::__invoke(func, lockedCallback, std::forward<Args>(args)...);
            }   
        }
    }

private:
    virtual void callbackErased(CallbackWeekPtr) {}
    
    CallbackList removeExpiredCallbacks()
    {
        CallbackList liveCallbacks;
        {
            std::lock_guard<std::mutex> lock(mDataLock);

            if (mCallbacks.empty())
            {
                return {};
            }

            liveCallbacks.reserve(mCallbacks.size());
            for(auto callbackIter = mCallbacks.begin(); callbackIter != mCallbacks.end(); ++callbackIter)
            {
                auto callback = *callbackIter;
                if(!callback.expired())
                {
                    liveCallbacks.push_back(callback);
                }
                else
                {
                    callbackErased(callback);
                }
            }

            if(mCallbacks.size() != liveCallbacks.size())
            {
                mCallbacks = liveCallbacks;
            }
        }  

        return liveCallbacks;          
    }
};


template <typename CallbackT>
class StringAssociatedNotificationHelper : public NotificationHelper<CallbackT>
{
public:
    virtual void registerCallback(typename NotificationHelper<CallbackT>::CallbackPtr callback, const std::vector<std::string>& associations,
     bool isImportantCallback = false)
    {
        {
            std::lock_guard<std::mutex> lock(NotificationHelper<CallbackT>::mDataLock);
            mCallBackAssociations[callback] = {associations, isImportantCallback};
        }
        NotificationHelper<CallbackT>::registerCallback(callback);
    }

    void fireNotification(typename NotificationHelper<CallbackT>::Closure closure, const std::string& associations, bool onlyImportantCallbacks = false)
    {
        CallBackAssociationsMap copiedCallbacks;
        {
            std::lock_guard<std::mutex> lock(NotificationHelper<CallbackT>::mDataLock);
            copiedCallbacks = mCallBackAssociations;
        }

        auto associationFilter = [&copiedCallbacks, &associations, onlyImportantCallbacks](typename 
            NotificationHelper<CallbackT>::CallbackWeekPtr callback) -> bool
        {
            auto foundCallback = copiedCallbacks.find(callback);
            if(foundCallback != copiedCallbacks.end())
            {
                const auto& callBackData = (*foundCallback).second;
                if(onlyImportantCallbacks && !callBackData.isImportantCallback)
                {
                    return false;
                }
                return std::find(callBackData.associations.begin(), callBackData.associations.end(), associations) != callBackData.associations.end();
            }

            return false;
        };

        NotificationHelper<CallbackT>::potentiallyFireNotification(closure, associationFilter);
    }

    void callbackErased(typename NotificationHelper<CallbackT>::CallbackWeekPtr callback) override
    {
        mCallBackAssociations.erase(callback);        
    }

private:
    struct CallBackData
    {
        CallBackData() = default;
        CallBackData(const std::vector<std::string>& associations, bool isImportantCallback) 
        : associations(associations), isImportantCallback(isImportantCallback)
        {
        }    

        std::vector<std::string> associations;
        bool isImportantCallback {false};
    };
    
    using BaseCallbackWeekPtr = typename NotificationHelper<CallbackT>::CallbackWeekPtr;
    using CallBackAssociationsMap = std::map<BaseCallbackWeekPtr, CallBackData, std::owner_less<BaseCallbackWeekPtr>>;
    CallBackAssociationsMap mCallBackAssociations;
};
